// TextSuggest
// Alex McClung
// Version 0.1
// 
// Suggest-as-you-type javascript class.
// REQUIREMENTS: Prototype Framework v.1.5.1.1
var TextSuggest = Class.create();

TextSuggest.prototype = {
	
	// ========================================================================
	// Constants
	TRACE_STRING: "TextSuggest :: ",
	
	// Keycode constants
	KEY_UP:    38,
	KEY_DN:    40,
	KEY_ENTER: 13,
	KEY_ESC:   27,
	
	// Timeouts (all values are in ms)
	HIDE_TIMEOUT: 250, // How long the suggestion box stays visible once it loses focus
	
	// Styles
	CSS_SUGGESTION_CONTAINER: "suggestion_container", // CSS class that gets applied to the output element
	CSS_SUGGESTION_HILIGHTED: "suggestion_hilighted", // CSS class that gets applied to highlighted suggestions
	CSS_SUGGESTION:           "suggestion",           // CSS class that gets applied to un-highlighted suggestions
	
	// Misc
	MARKUP_QUERY_TAG: /\{\$S\}/g, // {$S} Special character sequence used in the suggestion markup, will be replaced with the suggested text
	MARKUP_INDEX_TAG: /\{\$I\}/g, // {$I} Special character sequence used in the suggestion markup, will be replaced with the suggested text's index
	
	// ========================================================================
	// Private Properties
	mInput:       null, // Text input element that we monitor
	mOutput:      null, // HTML element that will contain the search suggestions
	mDataSource:  null, // Data source for text suggestion strings
	mSuggestions: null, // Array of suggested strings
	mHighlighted: null, // Current highlighted suggestion
	mSearchValue: null, // Value that the user has entered into the search field
	
	// This is the markup that is used to build a suggestion. This can be changed with SetSuggestionMarkup function.
	mSuggestionMarkup: "<a class=\"suggestion\" onmouseover=\"highlight({$I})\">{$S}</a>\n",
	
	// ========================================================================
	// Initialize
	
	// input      (HTMLElement or String) Reference to the text input to monitor
	// dataSource (mixed) Data source that provides a list of suggestion strings
	// output     (HTMLElement or String) (optional) Reference to an HTML element that will contain the search suggestions
	initialize: function(input, datasource, output)
	{
		// Initialize the input
		this._initInput(input);
		
		// Initialize the data source
		this._initDataSource(datasource);
		
		// Initialize the output field
		this._initOutput(output);
		
		// Init other properties
		this.mSuggestions = [];
		this.mHighlighted = -1;
	},
	
	// ========================================================================
	// Public Functions
	
	// ------------------------------------------------------------------------
	// Sets the markup to use when building a suggestion.
	// sMarkup (String) The markup to use. The special sequence {SUGG} will be
	//         replaced with the suggestion returned by the data source.
	setSuggestionMarkup: function(sMarkup)
	{
		this.mSuggestionMarkup = sMarkup;
	},
	
	// ------------------------------------------------------------------------
	// Highlights a suggestion in the suggestion box
	// iSuggestion (int) Index of the item to highlight
	highlightSuggestion: function(iSuggestion)
	{
		var iCurSuggestion = 0;
		
		// Loop through all of the elements
		for(var i=0; i < this.mOutput.childNodes.length; i++)
		{
			// Is this an anchor tag?
			if(this.mOutput.childNodes[i].tagName == 'A')
			{
				// Highlight it if the current index is the index to highlight, otherwise, un-highlight it
				this.mOutput.childNodes[i].className = (iCurSuggestion == iSuggestion) ? this.CSS_SUGGESTION_HILIGHTED : this.CSS_SUGGESTION;
				iCurSuggestion++;
			}
		}
		
		if(iSuggestion != -1)
		{
			// Put the current suggestion into the search field
			this.mInput.value = this.mSuggestions[iSuggestion];
		}
	},
	
	// ========================================================================
	// Private Functions
	
	// ------------------------------------------------------------------------
	// 	Initializes the input field
	_initInput: function(input)
	{
		// Capture the input
		this.mInput = (typeof(input)  == 'string') ? $(input)  : input;
		
		// Make sure it extends the prototype
		Element.extend(this.mInput);
		
		// Turn off autocomplete
		this.mInput.setAttribute('autocomplete', 'off');
		
		// Assign keyUp and keyDown event listeners for the element
		this.mInput.onkeydown = function(ev)
		{
			try      { this._searchKeyDown(event.keyCode); }
			catch(e) { this._searchKeyDown(ev.keyCode);    }
		}.bind(this);

		this.mInput.onkeyup = function(ev)
		{
			try      { this._searchKeyUp(event.keyCode); }
			catch(e) { this._searchKeyUp(ev.keyCode);    }
		}.bind(this);

		// Add a blur event listener that will hide the suggestion box after some interval
		this.mInput.onblur = function()
		{
			setTimeout(this._hideBox.bind(this), this.HIDE_TIMEOUT);
		}.bind(this);
		
		// If the output's been set...
		if(this.mOutput != null)
		{
			// Position it
			_positionOutput();
		}
	},
	
	// ------------------------------------------------------------------------
	// 	Initializes the output field
	_initOutput: function(output)
	{
		// Did the user supply an output field?
		if(typeof(output) != 'undefined')
		{
			// Capture the output field
			this.mOutput = (typeof(output) == 'string') ? $(output) : output;
		}
		// The user did not supply an output field
		else
		{
			// SO CREATE ONE
			var sID      = this.mInput.getAttribute("id") + "_suggestions";
			this.mOutput = document.createElement('div', sID);
			document.body.appendChild(this.mOutput);
		}
		
		// Make sure it extends the prototype
		Element.extend(this.mOutput);
		
		// Style the output element, make sure it is invisible
		this.mOutput.className = this.CSS_SUGGESTION_CONTAINER;
		this.mOutput.hide();
		
		// If the input's been set...
		if(this.mInput != null)
		{
			// Position the output window
			this._positionOutput();
		}
	},

	// ------------------------------------------------------------------------
	// 	Initializes the data source
	_initDataSource: function(datasource)
	{
		// Is the data supplier just an array?
		if(datasource instanceof Array)
		{
			// Create the simple data supplier
			this.mDataSource = new TextSuggestDataSource(datasource);
		}
		// It ought to be a TextSuggestDataSupplier
		else
		{
			this.mDataSource = datasource;
		}
		
		// Hey! Listen!
		this.mDataSource.onSuccess = this._onDataSourceSuccess.bind(this);
	},
	
	// ------------------------------------------------------------------------
	// Hides the suggestion box
	_hideBox: function()
	{
		this.mOutput.hide();
	},

	// ------------------------------------------------------------------------
	// Shows the suggestion box
	_showBox: function()
	{
		this.mOutput.show();
	},
	
	// ========================================================================
	// Event Handlers
	
	// ------------------------------------------------------------------------
	// Event listener; listens for keyboard key press events from the input field.
	// iKey (int) Keycode of the key that was pressed
	_searchKeyDown: function(iKey)
	{
		// Is the suggestion box open?
		if(this.mOutput.visible())
		{
			// Did they hit something other than the up or down key?
			if(iKey != this.KEY_UP && iKey != this.KEY_DN)
			{
				// Clear the current highlighted suggestion
				this.mHighlighted = -1;
			}
			// Did they hit up? Are we highlighting something?
			else if(iKey == this.KEY_UP && this.mHighlighted != -1)
			{
				// Move the suggestion cursor up
				this.mHighlighted--;
			}
			// Did they hit down? Are we not at the last suggestion on the list?
			else if(iKey == this.KEY_DN && this.mHighlighted < this.mSuggestions.length-1)
			{
				// Move the suggestion cursor down
				this.mHighlighted++;
			}
			
			// Highlight the new suggestion
			this.highlightSuggestion(this.mHighlighted);
				
			// Did they hit up? Did they move out of the cursor window? How about escape?
			if(
					(iKey == this.KEY_UP && this.mHighlighted == -1) ||
					(iKey == this.KEY_ESC)
			)
			{
				// Recall the old search value
				this.mInput.value = this.mSearchValue;
			}
		}
	},
	
	// ------------------------------------------------------------------------
	// Event listener; listens for keyboard key release events from the input field.
	// iKey (int) Keycode of the key that was pressed
	_searchKeyUp: function(iKey)
	{
		// Capture the new key value only if the user didn't hit up or down
		if(iKey != this.KEY_DN && iKey != this.KEY_UP && iKey != this.KEY_ENTER && iKey != this.KEY_ESC)
		{
			this.mSearchValue = this.mInput.value;
		}
		
		// Should we suggest something?
		if(this.mSearchValue != '' && iKey != this.KEY_ENTER && iKey != this.KEY_ESC && iKey != this.KEY_DN && iKey != this.KEY_UP)
		{
			// Grab them suggestions
			this.mDataSource.lookup( this.mInput.value.toLowerCase() );
		}
		
		// Did they clear the search value or hit escape, and are we showing the suggestion box?
		if( (this.mSearchValue == '' || iKey == this.KEY_ESC) && this.mOutput.visible() )
		{
			// Hide the suggestion box
			this._hideBox();
		}
	},
	
	// ------------------------------------------------------------------------
	// Event listener; listens for the data source's onComplete
	_onDataSourceSuccess: function()
	{
		// Get the suggestions from the data source
		this.mSuggestions = this.mDataSource.getSuggestions();
		
		// Did we find nothing?
		if(this.mSuggestions.length == 0)
		{
			// Hide the suggestion box
			this._hideBox();
		}
		// We've got some suggestions to display
		else
		{
			// Clear the suggestion box
			this.mOutput.innerHTML = '';
			var sMarkup = '';
			
			// Start building the panel
			for (var i=0; i < this.mSuggestions.length; i++)
			{
				sMarkup = this.mSuggestionMarkup.replace(this.MARKUP_QUERY_TAG, this.mSuggestions[i]);
				sMarkup = sMarkup.replace(this.MARKUP_INDEX_TAG, i);
				
				this.mOutput.innerHTML += "<div>" + sMarkup + "</div>";
			}
			
			// Show the suggestion box
			this._showBox();
		}
	},
	
	// ------------------------------------------------------------------------
	// Positions the output field underneath the input field
	_positionOutput: function()
	{
		// Get the input field's absolute offset
		var aOffset = Position.cumulativeOffset(this.mInput);
		var nInputHeight = this.mInput.getHeight();
		
		// Position the box underneath the input box
		this.mOutput.style.position = 'absolute';
		this.mOutput.style.top      = (aOffset[1] + nInputHeight) + "px";
		this.mOutput.style.left     = aOffset[0] + "px";
		this.mOutput.style.width    = (this.mInput.getWidth() - 2) + "px";
	}
};